package peerToPeerFacebook.proj;

import java.io.Serializable;

import peerToPeerFacebook.src.edu.washington.cs.cse490h.lib.Utility;

/** Implements class that provides remote procedure call abstraction on top of Paxos group. This class will try to
 * contact paxos servers in the group one by one until one of them replies. This class make sure to wait for previous
 * server to time out before contacting next one.
 * 
 * It is interesting to note, that in this case it is important that timeout signals that the server is down. In case
 * this is not true it is possible that same command will be executed twice on multiple servers. In case server crashes
 * in the middle of executing some command and this causes time out, it is still possible to have same command executed
 * twice.
 */
public class PaxosClient implements RemoteInvoker
{
    /** Class defining information about call that is being done */
    protected class RemoteCall
    {
        public IResultCallback callback = null;
        public String method = null;
        public Serializable[] params = null;
        public int currentServer = 0;
        
        public RemoteCall(String method, Serializable[] params, IResultCallback callback)
        {
            this.method = method;
            this.params = params;
            this.callback = callback;
        }
    }
    
    /** File name that stores sequance ID */
    protected static String DataFile = "paxos-client"; 
    
    /** Paxos group used for calls */
    protected RemoteInvoker[] paxosGroup = null;
    
    /** Definition of currently executing call or null if no such call exists. */
    protected RemoteCall outstandingCall = null;
    
    /** FB node for IP oeprations */
    protected FBNode node = null;
    
    /** COnstructs paxos invoker for the group.
     * 
     * @param paxosGroup List of servers that belong to the same paxos group.
     */
    public PaxosClient(FBNode node, RemoteInvoker[] paxosGroup)
    {
        assert paxosGroup.length > 0: "At least one member should be in paxos group";
        this.paxosGroup = paxosGroup;        
        this.node = node;
    }
    
    @Override
    public void invoke(String method, Serializable[] params, IResultCallback resultCallback)
    {
        assert outstandingCall == null: "Only one call is supported at any given time";        
        int sequenceID = 0;
        
        // Generate sequance ID
        if (Utility.fileExists(node, DataFile))
            sequenceID = Integer.parseInt(UtilityFile.readFromFile(node, DataFile));
        
        sequenceID++;
        UtilityFile.writeToFile(node, DataFile, Integer.toString(sequenceID));
        
        // update params to include sequanceID.
        Serializable[] fullParams = new Serializable[params.length + 1];
        fullParams [0] = sequenceID;
        for (int i = 0; i < params.length; i++)
            fullParams[i + 1] = params[i];
        
        outstandingCall = new RemoteCall(method, fullParams, resultCallback);
        pingServer();
    }
    
    @Override
    public boolean callPending()
    {
        return outstandingCall != null;
    }
    
    /** Pings server that is defined by outstandingCall.currServer. It also provides callback that handles result of
     * the call. If server timeouts than next server will be check (if such exists), otherwise result (including
     * exceptions) will be passed to the user.
     */
    protected void pingServer()
    {
        // Try to send call to current server and see what happens
        paxosGroup[outstandingCall.currentServer].invoke(outstandingCall.method, outstandingCall.params,
                new IResultCallback() {            
                    @Override
                    public void resultCallback(Status status, Serializable result)
                    {
                        // In case this is timeout error, see if we can ping next server on the list. If so, do it.
                        if (status == Status.Exception && result instanceof RPCTimeout &&
                                outstandingCall.currentServer < paxosGroup.length)
                        {
                            // need to ping next server
                            outstandingCall.currentServer++;
                            pingServer();
                        } else
                        {                        
                            // We either have result or this was last server on the list. In any case it is time to tell
                            // client what happen
                            RemoteCall tmp = outstandingCall;
                            outstandingCall = null;
                            tmp.callback.resultCallback(status, result);
                        }
                    }
        });
    }
}
